package com.gitee.wsl.mathematics.function.scale

import com.gitee.wsl.mathematics.interpolator.Interpolate
import com.gitee.wsl.math.interpolator.interpolateNumber
import com.gitee.wsl.math.interpolator.interpolateRound
import com.gitee.wsl.math.interpolator.uninterpolateNumber
import com.gitee.wsl.mathematics.function.scale.discrete.banded.BandScale
import com.gitee.wsl.mathematics.function.scale.discrete.banded.PointScale
import com.gitee.wsl.mathematics.function.scale.continuous.ContinuousScale
import com.gitee.wsl.mathematics.function.scale.continuous.SequentialScale
import com.gitee.wsl.mathematics.function.scale.discrete.OrdinalScale
import com.gitee.wsl.mathematics.function.scale.discrete.quantized.QuantileScale
import com.gitee.wsl.mathematics.function.scale.discrete.quantized.ThresholdScale
import com.gitee.wsl.mathematics.function.scale.continuous.linear.LinearScale
import com.gitee.wsl.mathematics.function.scale.continuous.linear.LogScale
import com.gitee.wsl.mathematics.function.scale.continuous.linear.PowerScale
import com.gitee.wsl.mathematics.function.scale.continuous.time.TimeScale
import com.gitee.wsl.mathematics.function.scale.discrete.quantized.QuantizeScale


/**
 * Generic signature of scales.
 *
 * A scale can map a domain object dimension DOMAIN to a representation value.
 *
 * Then at runtime, one can ask an RANGE object for a specific value of domain.
 * The rules defining the returns of RANGE from DOMAIN depends a lot on the type and
 * implementation of the Scale.
 *
 * a Domain object -> Range object
 */
 interface Scale<DOMAIN, out RANGE> {

     operator fun invoke(domainValue: DOMAIN): RANGE


    /**
     * Returns a copy of the current scale
     */
     fun copy(): Scale<DOMAIN, RANGE>

     companion object
}


/**
 * Indicates a scale for which the resulting R
 */
 interface ClampableScale {
     val clamp: Boolean
}

/**
 * Niceable scales implements [NiceableScale.nice] to possibly extends their domain to the nearest round values.
 *
 * Some niceableScale are: [LinearScale], [TimeScale], [LogScale] and [PowerScale]
 */
 interface NiceableScale<D> : ContinuousDomain<D> {

    /**
     * Extends the domain so that it starts and ends on nice round values.
     * This method typically modifies the scale’s domain, and may only extend the bounds to the nearest round value.
     * An optional tick count argument allows greater control over the step size used to extend the bounds,
     * guaranteeing that the returned ticks will exactly cover the domain. Nicing is useful if the domain is computed
     * from data, say using extent, and may be irregular. For example, for a domain of [0.201479…, 0.996679…],
     * a nice domain might be [0.2, 1.0]. If the domain has more than two values, nicing the domain only affects
     * the first and last value.
     *
     * Nicing a scale only modifies the current domain; it does not automatically nice domains that are
     * subsequently set using continuous.domain. You must re-nice the scale after setting the new domain, if desired.
     */
     fun nice(count: Int = 10)
}

 interface InvertableScale<D, R> : Scale<D, R> {
     fun invert(rangeValue: R): D
}

/**
 * Can provide ticks from Domain D.
 */
 interface Tickable<D> {
     fun ticks(count: Int = 10): List<D>
}

/**
 * Access the data2viz collection of scales.
 * To create a color scale use ScalesChomatic instead.
 * Available factories:
 *
 * Scales
 *      Continuous      (continuous domain to continuous range)
 *          -> linear
 *          -> log
 *          -> time
 *          -> ...
 *      Quantized       (continuous domain to discrete range)
 *          -> threshold
 *          -> quantize
 *          -> quantile
 *      Discrete        (discrete domain)
 *          -> ordinal
 *          -> band
 *          -> point
 */
object Scales {

     object Continuous {

        /**
         * Identity scales are a special case of linear scales where the domain and range are identical;
         * the scale and its invert method are thus the identity function. These scales are occasionally useful when
         * working with pixel coordinates, say in conjunction with an axis or brush.
         */
         fun identity(): LinearScale<Double> = LinearScale(Interpolate.Companion::interpolateNumber, Interpolate.Companion::uninterpolateNumber, naturalOrder()).apply {
            domain = listOf(.0, 1.0)
            range = listOf(.0, 1.0)
        }

         fun linear(init: LinearScale<Double>.() -> Unit = {}): LinearScale<Double> =
            LinearScale(Interpolate.Companion::interpolateNumber, Interpolate.Companion::uninterpolateNumber, naturalOrder()).apply(init)


         fun linearRound(init: LinearScale<Double>.() -> Unit = {}): LinearScale<Double> =
            LinearScale(Interpolate.Companion::interpolateRound, Interpolate.Companion::uninterpolateNumber, naturalOrder()).apply(init)

         fun log(
            base: Double = 10.0,
            init: ContinuousScale<Double, Double>.() -> Unit = {}
        ): com.gitee.wsl.mathematics.function.scale.continuous.linear.LogScale =
            com.gitee.wsl.mathematics.function.scale.continuous.linear.LogScale(
                base,
                Interpolate.Companion::interpolateNumber,
                Interpolate.Companion::uninterpolateNumber,
                naturalOrder()
            ).apply(init)

        /**
         * TODO Test
         */
         fun logRound(base: Double = 10.0): com.gitee.wsl.mathematics.function.scale.continuous.linear.LogScale =
            com.gitee.wsl.mathematics.function.scale.continuous.linear.LogScale(
                base,
                Interpolate.Companion::interpolateRound,
                Interpolate.Companion::uninterpolateNumber,
                naturalOrder()
            )


         fun pow(exponent: Double = 1.0, init: PowerScale<Double>.() -> Unit = {}): PowerScale<Double> =
            PowerScale(exponent, Interpolate.Companion::interpolateNumber, Interpolate.Companion::uninterpolateNumber, naturalOrder()).apply(init)

         fun powRound(exponent: Double = 1.0, init: PowerScale<Double>.() -> Unit = {}): PowerScale<Double> =
            PowerScale(exponent, Interpolate.Companion::interpolateRound, Interpolate.Companion::uninterpolateNumber, naturalOrder()).apply(init)


         fun sqrt(init: PowerScale<Double>.() -> Unit = {}): PowerScale<Double> =
            PowerScale(.5, Interpolate.Companion::interpolateNumber, Interpolate.Companion::uninterpolateNumber, naturalOrder()).apply(init)

         fun sqrtRound(init: PowerScale<Double>.() -> Unit = {}): PowerScale<Double> =
            PowerScale(.5, Interpolate.Companion::interpolateRound, Interpolate.Companion::uninterpolateNumber, naturalOrder()).apply(init)

         fun time(init: TimeScale<Double>.() -> Unit = {}): TimeScale<Double> =
            TimeScale(Interpolate.Companion::interpolateNumber, Interpolate.Companion::uninterpolateNumber, naturalOrder()).apply(init)

         fun sequential(Interpolate: Interpolate<Double>): SequentialScale<Double> = SequentialScale(Interpolate)

         // @JvmName("sequentialColor")
         //fun sequential(InterpolatorFun: InterpolatorFun<Color>): SequentialScale<Color> = SequentialScale(InterpolatorFun)
    }

     object Quantized {
         fun <R> quantile(init: QuantileScale<R>.() -> Unit = {}): QuantileScale<R> = QuantileScale<R>().apply(init)
         fun <R> quantize(init: QuantizeScale<R>.() -> Unit = {}): QuantizeScale<R> = QuantizeScale<R>().apply(init)
         fun <R> threshold(init: ThresholdScale<R>.() -> Unit = {}): ThresholdScale<R> = ThresholdScale<R>().apply(init)
    }

     object Discrete {
         fun <D> point(init: PointScale<D>.() -> Unit = {}): PointScale<D> = PointScale<D>().apply(init)
         fun <D> band(init: BandScale<D>.() -> Unit = {}): BandScale<D> = BandScale<D>().apply(init)
         fun <D, R> ordinal(init: OrdinalScale<D, R>.() -> Unit = {}): OrdinalScale<D, R> = OrdinalScale<D, R>().apply(init)
    }
}

inline val Scale.Companion.Discrete get() = Scales.Discrete

inline val Scale.Companion.Quantized get() = Scales.Quantized

inline val Scale.Companion.Continuous get() = Scales.Continuous